package com.javadaemon.rpgengine.model;


import java.util.logging.Level;
import java.util.logging.Logger;
import org.newdawn.slick.Animation;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.SlickException;

import com.javadaemon.rpgengine.util.AnimationSet;
import com.javadaemon.rpgengine.util.ResourceManager;


/*
 Copyright (c) 2011, Morten Andersen
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.
 * The name of the author may not be used to endorse or promote products
 derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED. IN NO EVENT SHALL MORTEN ANDERSEN BE LIABLE FOR ANY
 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * This class encapsulates information about an actor (e.g. a game object that can have movement and/or more advanced AI.)
 * 
 * @author Somnium
 * 
 */
public class Actor {

    public static final float MOVEMENT_SPEED = ((float) ResourceManager.TILE_WIDTH) / 420; // Universal
											    // speed
											    // for
											    // now
											    // -
											    // moving
											    // 1
											    // tile
											    // takes
											    // 0,42
											    // seconds.

    protected int tileX, tileY; // Tile coordinates for the actor.

    protected int worldX, worldY; // "Free floating" coordinates, e.g. absolute
				  // pixel coordinates. Used for movement
				  // between two tiles.

    protected int destWorldX = 0, destWorldY = 0; // Destination coordinates,
						  // used during move

    protected int destTileX = 0, destTileY = 0; // Destination coordinates, used
						// during move

    protected DIRECTION movementDirection = DIRECTION.SOUTH; // Movement
							     // direction, used
							     // during move

    protected int cumulativeDelta = 0; // Cumulative delta, used during move to
				       // avoid accumulating rounding errors.

    protected ACTOR_STATE currentState = ACTOR_STATE.IDLE; // The current AI
							   // state of the Actor

    protected float speed = MOVEMENT_SPEED;

    protected AnimationSet animationSet;

    protected Animation currentAnimation;

    private String mapID; // The currently active map.
    
    private boolean needsTile; // Wether this Actor needs to be assigned a tile

    /**
     * This enum encapsulates the possible AI states of an actor
     */
    public enum ACTOR_STATE {
	IDLE, // Actor is idle - for player actors, it is ready to respond to
	      // user input.
	MOVING, // Actor is moving - for player actors, ignore certain commands,
		// including new movement commands.
	;
    }

    /**
     * Constructor - Create an Actor instance
     * 
     * @param tileX
     * @param tileY
     * @param animationSet
     * @throws SlickException
     */
    public Actor(int tileX, int tileY, String mapID, AnimationSet animationSet)
	    throws SlickException {
	// Coordinates
	// Once the game supports multiple maps, a "map id" should also be added
	// to know which map the actor is on.
	this.tileX = tileX;
	this.tileY = tileY;
	this.mapID = mapID;
	this.worldX = tileX * ResourceManager.TILE_WIDTH;
	this.worldY = tileY * ResourceManager.TILE_HEIGHT;

	// Assign the animation set, and choose the default "still-frame"
	// animation to draw. It is assumed that actors
	// always face south. This can later be changed with a facing, if
	// necessary.
	this.animationSet = animationSet;
	this.currentAnimation = ResourceManager.getRessourceManager()
		.getAnimation(animationSet.getAnimation(DIRECTION.SOUTH));
	setFacingDirection(DIRECTION.SOUTH);
    }

    /**
     * Signals to the actor that it should commence moving. This updates the actor state, and initializes the movement attributes.
     * 
     * @param tileDestX
     *            The X-coordinate of the tile destination.
     * @param tileDestY
     *            The Y-coordinate of the tile destination
     * @param direction
     *            The direction of the move
     * @throws SlickException
     */
    public void initiateMove(int tileDestX, int tileDestY, TileMap map,
	    DIRECTION direction) throws SlickException {
	currentState = ACTOR_STATE.MOVING; // Set state to moving, so that the actors location will be updated during update.
	this.destTileX = tileDestX; // Set destination
	this.destTileY = tileDestY; // Set destination

	Tile tile = map.getTile(tileDestX, tileDestY);
	tile.setActor(this); // Set the Actor on the new tile
	tile = map.getTile(tileX, tileY);
	tile.setActor(null); // Delete the old reference

	destWorldX = tileDestX * ResourceManager.TILE_WIDTH; // Set destination in absolute pixels (for convenience)
	destWorldY = tileDestY * ResourceManager.TILE_HEIGHT; // Set destination in absolute pixels (for convenience)

	// Get the animation to be used, using the actor's animation set,
	// combined with the movement direction.
	currentAnimation = ResourceManager.getRessourceManager().getAnimation(
		animationSet.getAnimation(direction));
	movementDirection = direction; // Remember the movement direction
    }

    /**
     * Signals to the actor that movement has finished. This updates the actor state, and performs cleanup of the movement attributes.
     */
    public void finishMove(TileMap map) {
	currentState = ACTOR_STATE.IDLE; // Update state of actor so he is no longer moving.
	tileX = destTileX;
	tileY = destTileY;
	map.getTile(tileX, tileY).onApproach(this, map); // Execute any tile related actions
	worldX = tileX * ResourceManager.TILE_WIDTH; // Recalculate pixel coordinates
	worldY = tileY * ResourceManager.TILE_HEIGHT; // Recalculate pixel coordinates
	setFacingDirection(movementDirection);

	cumulativeDelta = 0; // Cleanup
	destTileX = 0; // Cleanup
	destTileY = 0; // Cleanup
	destWorldX = 0; // Cleanup
	destWorldY = 0; // Cleanup
	// movementDirection = null; // Cleanup
    }

    /**
     * Updates the actor - performs movement and other AI actions. The specific activity is determined by the actor state.
     * 
     * This should be called for each actor each "update tick".
     * 
     * @param container
     * @param delta
     */
    public void update(GameContainer container, int delta, TileMap map) {
	// Assign tile if needed
	if (needsTile) {
	    map.getTile(tileX, tileY).setActor(this);
	    needsTile = false;
	}
	
	// Move Actor
	if (Actor.ACTOR_STATE.MOVING.equals(currentState)) {
	    // Continue movement - add the speed times the delta to the current
	    // location, in the directions in which there is movement.
	    cumulativeDelta = cumulativeDelta + delta;
	    worldX = (int) ((tileX * ResourceManager.TILE_WIDTH) + ((movementDirection
		    .DX()) * (speed * cumulativeDelta)));
	    worldY = (int) ((tileY * ResourceManager.TILE_HEIGHT) + ((movementDirection
		    .DY()) * (speed * cumulativeDelta)));
	    currentAnimation.update(delta); // Since we do not auto-update the
					    // animations, we need to update the
					    // animation manually each
					    // "update tick"
	    // Finish move if destination is reached.
	    if (worldX <= destWorldX && movementDirection.DX() < 0) {
		finishMove(map);
	    } else if (worldX >= destWorldX && movementDirection.DX() > 0) {
		finishMove(map);
	    } else if (worldY <= destWorldY && movementDirection.DY() < 0) {
		finishMove(map);
	    } else if (worldY >= destWorldY && movementDirection.DY() > 0) {
		finishMove(map);
	    }
	}
    }

    // **** Getters and setters ****

    public ACTOR_STATE getCurrentState() {
	return currentState;
    }

    public int getWorldX() {
	return worldX;
    }

    public void setWorldX(int x) {
	worldX = x;
    }

    public int getWorldY() {
	return worldY;
    }

    public void setWorldY(int y) {
	worldY = y;
    }

    public int getTileX() {
	return tileX;
    }

    public void setTileX(int x) {
	this.tileX = x;
    }

    public int getTileY() {
	return tileY;
    }

    public void setTileY(int y) {
	this.tileY = y;
    }

    public String getMapId() {
	return mapID;
    }

    public void setMap(String mapID) {
	this.mapID = mapID;
    }

    public Animation getCurrentAnimation() {
	return currentAnimation;
    }

    public DIRECTION getCurrentDirection() {
	return movementDirection;
    }

    /**
     * Since this is a facing it doesn't just update one variable. We also need to make sure the frame in the current Animation is the one where our Actor is
     * standing still.
     * 
     * @param direction
     */
    public void setFacingDirection(DIRECTION direction) {
	try {
	    movementDirection = direction;
	    currentAnimation = ResourceManager.getRessourceManager()
		    .getAnimation(animationSet.getAnimation(direction));
	} catch (SlickException ex) {
	    Logger.getLogger(Actor.class.getName()).log(Level.SEVERE, null, ex);
	}
	// Make sure the actor is standing with it's legs together
	switch (movementDirection) {
	case NORTH:
	case SOUTH:
	    currentAnimation.setCurrentFrame(1);
	    break;
	default:
	    currentAnimation.restart();
	    break;
	}
    }
    
    public void setTileNeed(boolean tileNeed) {
	this.needsTile = tileNeed;
    }
}
